﻿
using EmperialApps.WeatherSpark.Data.Internal;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Xml;

namespace EmperialApps.WeatherSpark.Data {

    using DataWithPrecision = Pair<DataValues, sbyte>;
    using RawDataValues = ReadOnlyCollection<double>;

    /// <summary>Represents weather forecast information for a geographic location.</summary>
    public sealed partial class Forecast {

        /// <summary>Initializes a new instance of the <see cref="Forecast"/> class with the specified temperature data.</summary>
        public Forecast( Coordinate location, string description, DateTimeOffset start, RawDataValues temperature )
            : this( location, description, start, 1, temperature ) { }

        /// <summary>Initializes a new instance of the <see cref="Forecast"/> class with the specified temperature data.</summary>
        public Forecast( Coordinate location, string description, DateTimeOffset start, int interval, RawDataValues temperature )
            : this( location, description, start, interval, new[] { new DataValues( temperature, interval ) } ) { }

        /// <summary>Initializes a new instance of the <see cref="Forecast"/> class with the specified data.</summary>
        public Forecast( Coordinate location, string description, DateTimeOffset start, int interval, IDictionary<ForecastData, RawDataValues> dataValues )
            : this( location, description, start, interval, GetDataArray( dataValues, interval ) ) { }


        /// <summary>Gets the hourly temperature forecast.</summary>
        public DataValues Temperature {
            get { return GetDataValues( this.Data, ForecastData.Temperature ); }
        }

        /// <summary>Gets the hourly chance of precipitation.</summary>
        public DataValues PrecipitationPotential {
            get { return GetDataValues( this.Data, ForecastData.PrecipitationPotential ); }
        }

        /// <summary>Gets the hourly amount of clouds covering the sky.</summary>
        public DataValues SkyCover {
            get { return GetDataValues( this.Data, ForecastData.SkyCover ); }
        }

        /// <summary>Gets the hourly relative humidity.</summary>
        public DataValues RelativeHumidity {
            get { return GetDataValues( this.Data, ForecastData.RelativeHumidity ); }
        }

        /// <summary>Gets the hourly wind speed.</summary>
        public DataValues WindSpeed {
            get { return GetDataValues( this.Data, ForecastData.WindSustainedSpeed ); }
        }

        /// <summary>Gets the hourly wind direction.</summary>
        public DataValues WindDirection {
            get { return GetDataValues( this.Data, ForecastData.WindDirection ); }
        }

        /// <summary>Gets the hourly atmospheric pressure.</summary>
        public DataValues Pressure {
            get { return GetDataValues( this.Data, ForecastData.Pressure ); }
        }


        /// <summary>Adds redirect information to the forecast's description.</summary>
        public Forecast Redirect( string redirectUri ) {
            if( string.IsNullOrEmpty( redirectUri ) )
                return this;

            string description = RedirectPrefix + redirectUri;
            return new Forecast( this.Location, description, this.Start, this.Interval, this.Data );
        }

        /// <summary>Retrieves the new download URI for the forecast, if present.</summary>
        public bool TryGetRedirectUri( out string redirectUri ) {
            if( this.Description.Length > RedirectPrefix.Length
             && this.Description.Substring( 0, RedirectPrefix.Length ) == RedirectPrefix )
                redirectUri = this.Description.Substring( RedirectPrefix.Length );
            else
                redirectUri = null;

            return !string.IsNullOrEmpty( redirectUri );
        }


        /// <summary>Saves the forecast to the specified binary stream.</summary>
        public void Save( Stream destination ) {
            Format format = Format.Uncompressed;
            if( this.Interval % 2 == 0 )
                format |= Format.TwoHourInterval;
            if( this.Interval % 3 == 0 )
                format |= Format.ThreeHourInterval;

            destination.WriteByte( (byte)format );
            this.SaveBinary( destination );
        }

        /// <summary>Saves the forecast to the specified binary stream.</summary>
        public static void Save( Stream destination, Forecast forecast ) {
            forecast.Save( destination );
        }

        /// <summary>Reads a forecast from the specified binary or xml stream.</summary>
        public static Forecast Load( Stream source ) {
            return Load( source, fallbackLocation: null );
        }

        /// <summary>Reads a forecast from the specified binary or xml stream.</summary>
        public static Forecast Load( Stream source, Coordinate? fallbackLocation ) {
            var peekStream = new PeekableStream( source );
            int formatHeader = peekStream.PeekByte( );
            Format format = (Format)formatHeader;

            // If stream is binary, read source from updated Position;
            //  otherwise, read xml from peek stream at original Position.
            bool isBinaryFormat = (format & Format.IsBinaryFormat) == Format.IsBinaryFormat;
            try {
                Forecast forecast =
                    isBinaryFormat
                        ? LoadBinary( source, format )
                        : LoadXml( peekStream, fallbackLocation );

                return forecast;
            }
            catch( FormatException ) {
                throw;
            }
            catch( Exception ex ) {
                throw new FormatException( "Could not load forecast from stream.", ex );
            }
        }

        /// <summary>Trims extra data from the end of the forecast, up to the specified number of hours.</summary>
        public static Forecast Trim( Forecast forecast, int maximumHours ) {
            int maximumCount = (int)Math.Ceiling( (double)maximumHours / forecast.Interval );
            if( forecast.Temperature.Count <= maximumCount )
                return forecast;

            var data = new Dictionary<ForecastData, RawDataValues>( );
            for( int dataIndex = 0; dataIndex <= IndexLimit; ++dataIndex ) {
                ForecastData flag = GetDataForIndex( dataIndex );
                var dataValues = GetDataValues( forecast.Data, dataIndex );
                if( dataValues.Count == 0 )
                    continue;

                double[] trimmedValues = new double[maximumCount];
                for( int i = 0; i < maximumCount; ++i )
                    trimmedValues[i] = dataValues[i];

                data.Add( flag, trimmedValues.ToReadOnly( ) );
            }

            Forecast trimmedForecast = new Forecast( forecast.Location, forecast.Description, forecast.Start, forecast.Interval, data );
            return trimmedForecast;
        }

        /// <summary>Combines the data in the current forecast with another, up to the specified number of hours.</summary>
        public static Forecast Combine( Forecast first, Forecast second, int maximumHours = (int)(10.5 * ConvertValue.HoursPerDay) ) {
            // Order forecasts from earlier to later.
            if( first.Start > second.Start
            || (first.Start == second.Start && first.Temperature.Count * first.Interval > second.Temperature.Count * second.Interval) )
                Pair.Swap( ref first, ref second );

            // Determine how much data to take from each forecast.
            int interval = second.Interval;
            int secondCount = second.Temperature.Count;
            int firstHours = (int)(second.Start - first.Start).TotalHours;
            int maximumCount = maximumHours / interval;
            int count = Math.Min( maximumCount, firstHours / interval + secondCount );
            int hourOffset = (secondCount - count) * interval;

            // If later forecast contains all required hours, or earlier forecast does not reach later, return later.
            if( hourOffset == 0 || (first.End( ) < second.Start && secondCount < maximumCount) )
                return second;

            // Otherwise, create forecast from combined data.
            var data = new Dictionary<ForecastData, RawDataValues>( );
            for( int dataIndex = 0; dataIndex <= IndexLimit; ++dataIndex ) {
                ForecastData flag = GetDataForIndex( dataIndex );
                var firstData = GetDataValues( first.Data, dataIndex );
                var secondData = GetDataValues( second.Data, dataIndex );
                if( firstData.Count + secondData.Count == 0 )
                    continue;

                double[] values = new double[count];
                for( int i = 0; i < values.Length; ++i ) {
                    int secondHour = i * interval + hourOffset;
                    int secondIndex = secondHour / interval;
                    int firstIndex = (secondHour + firstHours) / first.Interval;

                    if( (uint)secondIndex < secondData.Count )
                        values[i] = secondData[secondIndex];
                    else if( firstIndex < firstData.Count )
                        values[i] = firstData[firstIndex];
                    else
                        values[i] = double.NaN;
                }

                data.Add( flag, values.ToReadOnly( ) );
            }

            DateTimeOffset start = second.Start.AddHours( hourOffset );
            Forecast forecast = new Forecast( second.Location, second.Description, start, interval, data );
            return forecast;
        }

        /// <inheritdoc/>
        public override string ToString( ) {
            return this.Description + " (" + this.Location + "), Start=(" + this.Start + ") [" + this.Interval + "], Data=" + this.Temperature.Count;
        }

        #region Private Members

        private static readonly string RedirectPrefix = "Please update to version 2.7 or later for latest forecast. " + Place.UriSeparator;
        private static readonly double[] Empty = new double[0];


        #region Data Indexing

        private const int IndexLimit = 6;

        private static int GetIndexForData( ForecastData data ) {
            switch( data ) {
                case ForecastData.Temperature:
                    return 0;
                case ForecastData.PrecipitationPotential:
                    return 1;
                case ForecastData.SkyCover:
                    return 2;
                case ForecastData.RelativeHumidity:
                    return 3;
                case ForecastData.WindSustainedSpeed:
                    return 4;
                case ForecastData.WindDirection:
                    return 5;
                case ForecastData.Pressure:
                    return 6;

                default:
                    return -1;
            }
        }

        private static ForecastData GetDataForIndex( int index ) {
            switch( index ) {
                case 0:
                    return ForecastData.Temperature;
                case 1:
                    return ForecastData.PrecipitationPotential;
                case 2:
                    return ForecastData.SkyCover;
                case 3:
                    return ForecastData.RelativeHumidity;
                case 4:
                    return ForecastData.WindSustainedSpeed;
                case 5:
                    return ForecastData.WindDirection;
                case 6:
                    return ForecastData.Pressure;

                case -1:
                default:
                    return 0;
            }
        }

        #endregion

        private static DataValues GetDataValues( DataValues[] data, ForecastData item ) {
            int index = GetIndexForData( item );
            return GetDataValues( data, index );
        }

        private static DataValues GetDataValues( DataValues[] data, int index ) {
            DataValues values =
                index < 0 || index >= data.Length
                    ? null
                    : data[index];
            return values ?? new DataValues( Empty, data[0].Interval );
        }

        private static DataValues[] GetDataArray( IDictionary<ForecastData, RawDataValues> data, int interval ) {
            int length = 0;
            var indexedData = new Dictionary<int, DataValues>( );
            foreach( var kvp in data ) {
                int index = GetIndexForData( kvp.Key );
                if( index >= 0 ) {
                    length = Math.Max( length, 1 + index );
                    indexedData.Add( index, new DataValues( kvp.Value, interval ) );
                }
            }

            var dataArray = new DataValues[length];
            foreach( var kvp in indexedData )
                dataArray[kvp.Key] = kvp.Value;

            return dataArray;
        }


        #region Save/Load

        private void SaveBinary( Stream destination ) {
            var dataValues = new List<DataWithPrecision>( );
            ForecastData data = 0;
            for( int i = 0; i < this.Data.Length; ++i )
                data |= AddDataItem( dataValues, this.Data[i], GetDataForIndex( i ) );

            var writer = new BinaryWriter( destination );

            writer.Write( data );
            writer.Write( this.Location );
            writer.Write( this.Description );
            writer.Write( this.Start );

            foreach( var pair in dataValues )
                writer.Write( pair.Item1, pair.Item2 );
        }

        private static Forecast LoadBinary( Stream source, Format format ) {
            int interval = 1;
            if( (format & Format.TwoHourInterval) == Format.TwoHourInterval )
                interval *= 2;
            if( (format & Format.ThreeHourInterval) == Format.ThreeHourInterval )
                interval *= 3;

            var reader = new BinaryReader( source );

            var data = reader.ReadForecastData( );
            var location = reader.ReadCoordinate( );
            var description = reader.ReadString( );
            var start = reader.ReadDateTimeOffset( );

            var supportedData = data & (Dwml.SupportedData | Weatherdata.SupportedData);
            var dataValues = new Dictionary<ForecastData, RawDataValues>( );
            foreach( ForecastData flag in AllFlags ) {
                double[] values = ReadDataItem( reader, data, flag );
                if( values.Length > 0 && HasFlag( supportedData, flag ) )
                    dataValues.Add( flag, values.ToReadOnly( ) );
            }

            return new Forecast( location, description, start, interval, dataValues );
        }


        private static Forecast LoadXml( Stream source, Coordinate? fallbackLocation ) {
            var reader = XmlReader.Create( new XmlSanitizingStream( source ) );

            while( reader.NodeType != XmlNodeType.Element )
                reader.Read( );

            switch( reader.Name ) {
                case Dwml.Identifier:
                    return Dwml.Load( reader, fallbackLocation );
                case Weatherdata.Identifier:
                    return Weatherdata.Load( reader, fallbackLocation );
                default:
                    throw new FormatException( "Unrecognized XML content: " + reader.Name );
            }
        }

        private static Coordinate LoadLocation( XmlReader reader, string elementName, Coordinate? fallbackLocation ) {
            reader.ReadToFollowing( elementName );
            string latitude = reader.GetAttribute( "latitude" );
            string longitude = reader.GetAttribute( "longitude" );
            double latitudeValue, longitudeValue = default( double );
            bool invalidLocation =
                   !double.TryParse( latitude, NumberStyles.Any, CultureInfo.InvariantCulture, out latitudeValue )
                || !double.TryParse( longitude, NumberStyles.Any, CultureInfo.InvariantCulture, out longitudeValue )
                || (latitudeValue == 0 && longitudeValue == 0)
                || (Math.Abs( latitudeValue ) > 90 && Math.Abs( longitudeValue ) > 180);

            var location =
                invalidLocation && fallbackLocation.HasValue
                    ? fallbackLocation.Value
                    : new Coordinate( latitudeValue, longitudeValue );
            return location;
        }

        private static double ParseDouble( string value, string collectionName ) {
            try { return double.Parse( value, CultureInfo.InvariantCulture ); }
            catch( FormatException ex ) {
                throw new FormatException( "Could not parse " + collectionName + " value from '" + value + "'.", ex );
            }
        }


        private struct Dwml {
            public const string Identifier = "dwml";
            public static readonly ForecastData SupportedData;
            private static readonly Dwml[] OrderedItems;
            private static readonly Dictionary<string, Dwml> Items;

            private readonly ForecastData Data;
            private readonly string CollectionName;
            private readonly string CollectionType;
            private readonly Func<double, double> Convert;

            private Dwml( ForecastData data, string collectionName, string collectionType, Func<double, double> convert ) {
                this.Data = data;
                this.CollectionName = collectionName;
                this.CollectionType = collectionType;
                this.Convert = convert;
            }

            [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2207:InitializeValueTypeStaticFieldsInline" )]
            static Dwml( ) {
                OrderedItems = new[] {
                    new Dwml( ForecastData.Temperature, "temperature", "hourly", ConvertValue.ToCelsius ),
                    new Dwml( ForecastData.PrecipitationPotential, "probability-of-precipitation", "floating", ConvertValue.ToDecimal ),
                    new Dwml( ForecastData.WindSustainedSpeed, "wind-speed", "sustained", ConvertValue.ToMetersPerSecond ),
                    new Dwml( ForecastData.WindDirection, "direction", "wind", ConvertValue.ToDirection ),
                    new Dwml( ForecastData.SkyCover, "cloud-amount", "total", ConvertValue.ToDecimal ),
                    new Dwml( ForecastData.RelativeHumidity, "humidity", "relative", ConvertValue.ToDecimal ),
                };

                Items = new Dictionary<string, Dwml>( OrderedItems.Length, StringComparer.OrdinalIgnoreCase );
                foreach( Dwml item in OrderedItems ) {
                    Items.Add( item.CollectionName, item );
                    SupportedData |= item.Data;
                }
            }


            public static Forecast Load( XmlReader reader, Coordinate? fallbackLocation ) {
                Coordinate location = LoadLocation( reader, "point", fallbackLocation );

                do
                    reader.Skip( );
                while( reader.NodeType != XmlNodeType.Element );
                string description = reader.ReadElementContentAsString( );

                reader.ReadToFollowing( "start-valid-time" );
                string startString = reader.ReadElementContentAsString( );
                var start = DateTimeOffset.Parse( startString, CultureInfo.InvariantCulture );

                var data = new Dictionary<ForecastData, RawDataValues>( );
                reader.ReadToFollowing( "parameters" );
                while( data.Count < Items.Count ) {
                    string collectionName, collectionType;
                    if( !ReadToNextCollection( reader, out collectionName, out collectionType ) ) {
                        foreach( var item in OrderedItems )
                            if( !data.ContainsKey( item.Data ) )
                                throw new FormatException( "File did not contain any " + item.CollectionName + " data." );
                    }

                    Dwml unread;
                    if( Items.TryGetValue( collectionName, out unread )
                     && Items.Comparer.Equals( collectionType, unread.CollectionType ) )
                        data[unread.Data] = unread.Load( reader, collectionName );
                }

                return new Forecast( location, description, start, 1, data );
            }

            private RawDataValues Load( XmlReader reader, string collectionName ) {
                reader.ReadToDescendant( "value" );

                double value = 0;
                var values = new List<double>( );
                do {
                    string stringValue = reader.ReadElementContentAsString( );
                    if( stringValue.Length > 0 )
                        value = this.Convert( ParseDouble( stringValue, collectionName ) );

                    values.Add( value );
                } while( reader.NodeType != XmlNodeType.EndElement );

                return values.ToReadOnly( );
            }

            private static bool TryGetElementAttribute( XmlReader reader, string attributeName, out string attributeValue ) {
                attributeValue =
                    reader.NodeType == XmlNodeType.Element
                        ? reader.GetAttribute( attributeName )
                        : null;

                return attributeValue != null;
            }

            private static bool ReadToNextCollection( XmlReader reader, out string collectionName, out string collectionType ) {
                while( reader.Read( ) )
                    if( TryGetElementAttribute( reader, "type", out collectionType ) ) {
                        collectionName = reader.Name;
                        return true;
                    }

                collectionName = null;
                collectionType = null;
                return false;
            }

            public override string ToString( ) {
                return string.Format( "DWML {0} data ({1} {2})", this.Data, this.CollectionType, this.CollectionName );
            }
        }

        private struct Weatherdata {
            public const string Identifier = "weatherdata";
            public static readonly ForecastData SupportedData;
            private static readonly Dictionary<string, Weatherdata> Items;

            private readonly ForecastData Data;
            private readonly string ElementName;
            private readonly string AttributeName;
            private readonly Func<double, double> Convert;

            private Weatherdata( ForecastData data, string elementName, string attributeName, Func<double, double> convert ) {
                this.Data = data;
                this.ElementName = elementName;
                this.AttributeName = attributeName;
                this.Convert = convert;
            }

            [System.Diagnostics.CodeAnalysis.SuppressMessage( "Microsoft.Usage", "CA2207:InitializeValueTypeStaticFieldsInline" )]
            static Weatherdata( ) {
                var items = new[] {
                    new Weatherdata( ForecastData.Temperature, "temperature", "value", ConvertValue.Identity ),
                    new Weatherdata( ForecastData.WindSustainedSpeed, "windSpeed", "mps", ConvertValue.Identity ),
                    new Weatherdata( ForecastData.WindDirection, "windDirection", "deg", ConvertValue.ToDirection ),
                    new Weatherdata( ForecastData.PrecipitationPotential | ForecastData.SkyCover, "symbol", "number", null ),
                    new Weatherdata( ForecastData.Pressure, "pressure", "value", ConvertValue.Identity ),
                };

                Items = new Dictionary<string, Weatherdata>( items.Length, StringComparer.OrdinalIgnoreCase );
                foreach( Weatherdata item in items ) {
                    Items.Add( item.ElementName, item );
                    SupportedData |= item.Data;
                }
            }


            public static Forecast Load( XmlReader reader, Coordinate? fallbackLocation ) {
                reader.ReadToFollowing( "name" );
                string name = reader.ReadElementContentAsString( );
                reader.ReadToFollowing( "country" );
                string country = reader.ReadElementContentAsString( );
                if( country == "United States" )
                    country = "US";

                reader.ReadToFollowing( "timezone" );
                string offsetMinutesString = reader.GetAttribute( "utcoffsetMinutes" );
                double offsetMinutes = ParseDouble( offsetMinutesString, "UTC Offset" );
                TimeSpan offset = TimeSpan.FromMinutes( offsetMinutes );

                Coordinate location = LoadLocation( reader, "location", fallbackLocation );
                string id = reader.GetAttribute( "geobaseid" );

                reader.ReadToFollowing( "link" );
                string url = reader.GetAttribute( "url" );
                string description = name + ", " + country + Place.UriSeparator + url.Insert( url.Length - 1, Place.IdSeparator + id );

                reader.ReadToFollowing( "forecast" );
                reader.ReadToFollowing( "tabular" );

                var data = new Dictionary<ForecastData, RawDataValues>( );
                var values = new Dictionary<ForecastData, List<double>>( );
                foreach( ForecastData flag in AllFlags ) {
                    if( HasFlag( SupportedData, flag ) )
                        data[flag] = new RawDataValues( values[flag] = new List<double>( ) );
                }

                int interval = 0;
                DateTimeOffset? start = null;
                while( reader.ReadToFollowing( "time" ) ) {
                    string fromString = reader.GetAttribute( "from" );
                    string toString = reader.GetAttribute( "to" );
                    var from = new DateTimeOffset( DateTime.Parse( fromString, CultureInfo.InvariantCulture ), offset );
                    var to = new DateTimeOffset( DateTime.Parse( toString, CultureInfo.InvariantCulture ), offset );

                    // Update interval, adjusting for offset of initial sample if necessary.
                    int hours = (to - from).Hours;
                    if( !start.HasValue ) {
                        if( hours <= 6 )
                            interval = hours;
                        start = from;
                    }
                    else if( hours > interval && hours <= 6 ) {
                        interval = hours;
                        start = from.AddHours( -interval );
                    }

                    // Read all data items for the current time sample.
                    while( reader.Read( ) && reader.NodeType != XmlNodeType.EndElement ) {
                        if( reader.NodeType != XmlNodeType.Element )
                            continue;

                        Weatherdata item;
                        string elementName = reader.Name;
                        if( Items.TryGetValue( elementName, out item ) )
                            item.Load( reader, values );
                    }
                }

                return new Forecast( location, description, start.Value, interval, data );
            }


            private void Load( XmlReader reader, Dictionary<ForecastData, List<double>> values ) {
                string valueString = reader.GetAttribute( this.AttributeName );
                double rawValue = ParseDouble( valueString, this.AttributeName );

                if( this.Convert != null ) {
                    double value = this.Convert( rawValue );
                    values[this.Data].Add( value );
                }
                else {
                    System.Diagnostics.Debug.Assert( this.Data == (ForecastData.PrecipitationPotential | ForecastData.SkyCover), "Only Symbol data should have a null convert delegate." );

                    YrnoSymbol symbol = (YrnoSymbol)rawValue;
                    values[ForecastData.SkyCover].Add( ConvertValue.ToSkyCover( symbol ) );
                    values[ForecastData.PrecipitationPotential].Add( ConvertValue.ToPrecipitationPotential( symbol ) );
                }
            }

            public override string ToString( ) {
                return string.Format( "YRNO {0} data ({1} {2})", this.Data, this.AttributeName, this.ElementName );
            }
        }


        private static IEnumerable<ForecastData> AllFlags {
            get {
                for( ForecastData flag = ForecastData.Temperature; flag <= ForecastData.Last; flag = (ForecastData)((int)flag << 1) )
                    yield return flag;
            }
        }

        private static bool HasFlag( ForecastData data, ForecastData flag ) {
            return (data & flag) == flag;
        }

        private static ForecastData AddDataItem( List<DataWithPrecision> dataValues, DataValues values, ForecastData data ) {
            ForecastData flag = 0;
            if( values != null && values.Count > 0 ) {
                sbyte precision;
                switch( data ) {
                    case ForecastData.Temperature:
                        precision = ConvertValue.TemperaturePrecision;
                        break;
                    case ForecastData.WindSustainedSpeed:
                        precision = ConvertValue.SpeedPrecision;
                        break;
                    case ForecastData.WindDirection:
                        precision = ConvertValue.DirectionPrecision;
                        break;
                    case ForecastData.Pressure:
                        precision = ConvertValue.PressurePrecision;
                        break;
                    case ForecastData.PrecipitationPotential:
                    case ForecastData.SkyCover:
                    case ForecastData.RelativeHumidity:
                        precision = ConvertValue.PercentagePrecision;
                        break;
                    default:
                        System.Diagnostics.Debug.Assert( false, "Unrecognized data flag: " + data );
                        precision = 0;
                        break;
                }

                dataValues.Add( Pair.Create( values, precision ) );
                flag = data;
            }

            return flag;
        }

        private static double[] ReadDataItem( BinaryReader reader, ForecastData data, ForecastData flag ) {
            return HasFlag( data, flag )
                 ? reader.ReadDoubleCollection( )
                 : Empty;
        }


        private static bool IsSupportedInterval( int interval ) {
            switch( interval ) {
                case 1:
                case 2:
                case 3:
                case 6:
                    return true;
                default:
                    return false;
            }
        }

        private enum Format : byte {
            UncompressedBinaryOneHourInterval = Uncompressed | IsBinaryFormat,
            UncompressedBinaryTwoHourInterval = Uncompressed | IsBinaryFormat | TwoHourInterval,
            UncompressedBinaryThreeHourInterval = Uncompressed | IsBinaryFormat | ThreeHourInterval,
            UncompressedBinarySixHourInterval = Uncompressed | IsBinaryFormat | TwoHourInterval | ThreeHourInterval,

            Uncompressed = IsBinaryFormat | 1 << 0,
            ThreeHourInterval = 1 << 1,
            TwoHourInterval = 1 << 2,

            IsBinaryFormat = 1 << 7,

            Empty = byte.MaxValue
        }

        #endregion

        #endregion
    }

}
